import utils from '../utils'
import protocols from './protocols'
import { camel2snakeJson, snake2camelJson, UniCloudError } from '../../shared'
import WxCrypto from './wx-crypto'

class PaymentBase {
  constructor (options) {
    const defaultOptions = {
      currencyType: 'CNY',
      rate: 100,
      timeout: 10000
    }
    this.options = {
      ...defaultOptions,
      ...options
    }

    if (!options.appId) throw new Error('appId required')
    if (!options.mchId) throw new Error('mchId required')
    if (!options.sandbox && !options.appKey) throw new Error('appKey required')
    if (options.sandbox && !options.sandboxAppKey) throw new Error('sandboxAppKey required')
    if (!options.offerId) throw new Error('offerId required')
    if (!options.accessToken) throw new Error('accessToken required')
    if (!options.token) throw new Error('token required')
    if (!options.encodingAESKey) throw new Error('encodingAESKey required')
    this.appKey = options.sandbox ? options.sandboxAppKey : options.appKey

    this._protocols = protocols
    this._baseURL = 'https://api.weixin.qq.com'
    this._wxCrypto = new WxCrypto({
      appId: options.appId,
      encodingAESKey: options.encodingAESKey,
      token: options.token
    })
  }

  /**
   * 请求
   * @param action
   * @param params
   * @param method
   * @param successStatusCode
   * @returns {Promise<*>}
   * @private
   */
  async _request (action, params, method = 'GET', successStatusCode = 200) {
    params = this._publicParams(params)
    params = camel2snakeJson(params)
    params = JSON.parse(JSON.stringify(params))

    if (this.options.debug) console.log('params', params)
    const body = JSON.stringify(params)
    const appKey = this.appKey
    const paySig = utils.getPaySig(action, body, appKey)
    const accessToken = this.options.accessToken
    let url = `${this._baseURL}${action}?access_token=${accessToken}&pay_sig=${paySig}`
    if (params.session_key) {
      const signature = utils.getSignature(body, params.session_key)
      url += `&signature=${signature}`
    }

    const { status, data = {} } = await uniCloud.httpclient.request(url, {
      method,
      data: params,
      headers: {
        'content-type': 'application/json'
      },
      dataType: 'json',
      timeout: this.options.timeout
    })
    if (status !== successStatusCode) {
      throw new UniCloudError({
        code: data.code,
        message: data.message
      })
    }
    if (data.errcode) data.errCode = data.errcode
    if (data.errmsg) data.errMsg = data.errmsg
    delete data.errcode
    delete data.errmsg
    if (!data.errCode) data.errCode = 0
    data.code = data.errCode
    if (data.errMsg) data.msg = data.errMsg
    if (data.errCode) {
      throw new UniCloudError({
        code: data.errCode,
        message: data.errMsg
      })
    }

    const res = snake2camelJson(data)
    if (!res.appId) res.appId = this.options.appId
    if (!res.mchId) res.mchId = this.options.mchId
    if (this.options.debug) console.log('res', res)
    return res
  }

  /**
   * 请求公共参数
   * @param params
   * @returns {any}
   * @private
   */
  _publicParams (params) {
    const publicParams = {
      env: this.options.sandbox ? 1 : 0
    }
    return Object.assign(publicParams, params)
  }

  /**
   * 验证响应签名
   * @param {Object} httpInfo
   * @returns {Promise<void>}
   * @private
   */
  async _verifyResponseSign (httpInfo) {
    const { queryStringParameters = {} } = httpInfo
    const {
      signature, // 签名
      timestamp, // 时间戳
      nonce // 随机数
    } = queryStringParameters
    const wxCrypto = this._wxCrypto
    return wxCrypto.verifyResponseSign({
      signature,
      timestamp,
      nonce
    })
    // const token = this.options.token
    // const tmpArr = [token, timestamp, nonce]
    // tmpArr.sort((a, b) => a.localeCompare(b))
    // const tmpStr = tmpArr.join('')
    // const mySignature = crypto.createHash('sha1').update(tmpStr).digest('hex')
    // const verify = mySignature === signature
    // return verify
  }

  /**
   * 解密消息
   * @param {Object} httpInfo
   * @returns {Promise<void>}
   * @private
   */
  async _decrypt (data = {}) {
    const {
      msgSignature,
      timestamp,
      nonce,
      body = {}
    } = data
    const {
      Encrypt: encrypt
    } = body
    const wxCrypto = this._wxCrypto
    const verifyMsgSign = wxCrypto.verifyMsgSign({
      timestamp,
      nonce,
      encrypt,
      msg_signature: msgSignature
    })
    if (!verifyMsgSign) {
      // 消息签名验证未通过
      return null
    }
    // 消息签名验证通过
    // 如果没有密文，则不需要解密
    if (!encrypt) {
      return body
    }
    // 解密
    const encryptData = wxCrypto.decrypt(encrypt)
    if (this.options.appId !== encryptData.appId) {
      // appId不一致
      return null
    }
    return encryptData.value
  }
}

/**
 * @param {String} options.appId 应用ID
 * @param {String} options.mchId 商户ID
 * @param {String} options.timeout=5000 请求超时时间
 */
class Payment extends PaymentBase {
  /**
   * 生成下单参数
   * @param {String} params.sessionKey
   * @param {String} params.mode 支付的类型, 不同的支付类型有各自额外要传的附加参数
   * @param {String} params.outTradeNo 商户订单号
   * @param {Number} params.buyQuantity 购买数量
   * @param {String} params.productId 道具ID
   * @param {Number} params.goodsPrice 道具单价(分)
   * @param {String} params.attach 透传数据, 发货通知时会透传给开发者
   * @returns {Promise<*>}
   */
  async getOrderInfo (params) {
    params = this._publicParams(params)
    const {
      sessionKey,
      mode,
      outTradeNo,
      buyQuantity,
      productId,
      goodsPrice,
      attach
    } = params

    if (!mode) throw new Error('mode required')
    if (mode === 'short_series_goods') {
      if (!productId) throw new Error('productId required')
      if (!goodsPrice) throw new Error('goodsPrice required')
    }
    if (!sessionKey) throw new Error('sessionKey required')
    if (!outTradeNo) throw new Error('outTradeNo required')
    if (!buyQuantity) throw new Error('buyQuantity required')

    const signData = JSON.stringify({
      offerId: this.options.offerId,
      env: this.options.sandbox ? 1 : 0,
      currencyType: this.options.currencyType,
      buyQuantity,
      outTradeNo,
      productId,
      goodsPrice,
      attach
    })

    const appKey = this.appKey
    const paySig = utils.getPaySig('requestVirtualPayment', signData, appKey)
    const signature = utils.getSignature(signData, sessionKey)

    const res = {
      signData, // 签名数据
      mode, // 代币充值 short_series_goods 道具直购 short_series_coin 代币充值
      paySig, // 支付签名
      signature // 用户态
    }
    return res
  }

  /**
   * 查询订单
   * @param {String} params.openid 用户的openid
   * @param {String} params.outTradeNo 商户订单号
   * @param {String} params.transactionId 平台订单号
   * @returns {Promise<*>}
   */
  async orderQuery (params) {
    return await this._request('/xpay/query_order', params, 'POST')
  }

  /**
   * 关闭订单(微信虚拟支付无此接口)
   * @param {String} params.outTradeNo 商户订单号
   * @returns {Promise<*>}
   */
  async closeOrder (params) {
    return {

    }
  }

  /**
   * 申请退款
   * @param {String} params.openid 下单时的用户openid
   * @param {String} params.outTradeNo 商户订单号
   * @param {String} params.transactionId 平台订单号
   * @param {String} params.outRefundNo 商户退款单号
   * @param {String} params.totalFee 订单总金额
   * @param {String} params.refundFee 退款总金额
   * @param {String} params.refundFeeType 货币种类
   * @param {String} params.refundDesc 退款原因
   * @param {String} params.leftFee 当前单剩余可退金额，单位分，可以通过调用query_order接口查到
   * @param {String} params.bizMeta 商家自定义数据，传入后可在query_order接口查询时原样返回，长度需要[0,1024]
   * @param {String} params.reqFrom 退款来源，当前仅支持以下值 1-人工客服退款，即用户电话给客服，由客服发起退款流程 2-用户自己发起退款流程 3-其它
   * @returns {Promise<*>}
   */
  async refund (params) {
    const res = await this._request('/xpay/refund_order', params, 'POST')
    if (res.errMsg === 'OK') {
      res.refundFee = params.refundFee
    }
    return res
  }

  /**
   * 退款查询与查询订单是同一个接口
   * @param {String} params.openid 用户的openid
   * @param {String} params.outTradeNo 商户订单号
   * @param {String} params.transactionId 平台订单号
   * @returns {Promise<*>}
   */
  async refundQuery (params) {
    return await this._request('/xpay/query_order', params, 'POST')
  }

  /**
   * 获取通知类型
   * @param httpInfo
   * @returns {String}
   */
  checkNotifyType (httpInfo) {
    let { body = {}, queryStringParameters = {} } = httpInfo
    if (httpInfo.isBase64Encoded) {
      body = Buffer.from(body, 'base64').toString('utf-8')
    }
    if (body && typeof body === 'string') {
      try {
        body = JSON.parse(body)
      } catch (err) {
        body = {}
      }
    }
    const { Event } = body
    if (!Event && queryStringParameters.echostr) {
      return 'token'
    }
    if (['xpay_coin_pay_notify', 'xpay_goods_deliver_notify'].indexOf(Event) > -1) {
      // 支付通知
      return 'payment'
    } else if (['xpay_refund_notify'].indexOf(Event) > -1) {
      // 退款通知
      return 'refund'
    }
    return 'unknown'
  }

  /**
   * 验证token通知
   * @param httpInfo
   * @returns {Promise<*>}
   */
  async verifyTokenNotify (httpInfo) {
    const { queryStringParameters = {} } = httpInfo
    const {
      echostr // 签名
    } = queryStringParameters
    // 请求合法验证
    const verify = await this._verifyResponseSign(httpInfo)
    if (!verify) {
      return null
    }
    const res = {
      appId: this.options.appId,
      mchId: this.options.mchId,
      echostr
    }
    return res
  }

  async _verifyNotify (httpInfo) {
    let { body = {} } = httpInfo
    if (httpInfo.isBase64Encoded) {
      body = Buffer.from(body, 'base64').toString('utf-8')
    }
    if (typeof body === 'string') {
      try {
        body = JSON.parse(body)
      } catch (err) {}
    }

    // 请求合法验证
    const verify = await this._verifyResponseSign(httpInfo)
    if (!verify) {
      return null
    }

    const { queryStringParameters = {} } = httpInfo
    const {
      msg_signature: msgSignature, // 消息签名
      timestamp, // 时间戳
      nonce // 随机数
    } = queryStringParameters

    // 参数合法验证
    const decrypted = this._decrypt({
      msgSignature,
      timestamp,
      nonce,
      body
    })

    const res = snake2camelJson(decrypted)
    res.appId = this.options.appId
    res.mchId = this.options.mchId
    return res
  }

  /**
   * 支付成功回调通知
   * @param httpInfo
   * @returns {Promise<*>}
   */
  async verifyPaymentNotify (httpInfo) {
    if (this.checkNotifyType(httpInfo) !== 'payment') {
      return false
    }
    return await this._verifyNotify(httpInfo)
  }

  /**
   * 退款回调通知
   * @param httpInfo
   * @returns {Promise<*>}
   */
  async verifyRefundNotify (httpInfo) {
    if (this.checkNotifyType(httpInfo) !== 'refund') {
      return false
    }
    return await this._verifyNotify(httpInfo)
  }

  /**
   * 通知已经发货完成（只能通知现金单）,正常通过xpay_goods_deliver_notify消息推送返回成功就不需要调用这个api接口。这个接口用于异常情况推送不成功时手动将单改成已发货状态
   * @param {String} params.outTradeNo 用户的openid
   * @param {String} params.transactionId 平台订单号
   * @returns {Promise<*>}
   */
  async notifyProvideGoods (params) {
    return await this._request('/xpay/notify_provide_goods', params, 'POST')
  }

  /**
   * 查询用户余额
   * @param {String} params.openid 用户的openid
   * @param {String} params.userIp 用户ip
   * @returns {Promise<*>}
   */
  async queryUserBalance (params) {
    if (!params.openid) throw new Error('参数 openid 必填')
    if (!params.userIp) throw new Error('参数 userIp 必填')
    return await this._request('/xpay/query_user_balance', params, 'POST')
  }

  /**
   * 扣减代币（一般用于代币支付）
   * @param {String} params.openid 用户的openid
   * @param {String} params.userIp 用户ip
   * @param {Number} params.amount 支付的代币数量
   * @param {String} params.outTradeNo 订单号order_id
   * @param {String} params.payitem 物品信息。记录到账户流水中。如:[{"productid":"物品id", "unit_price": 单价, "quantity": 数量}]
   * @param {String} params.remark 备注
   * @param {Number} params.deviceType 平台类型1-安卓 2-苹果
   * @param {String} params.sessionKey 微信小程序用户的sessionKey
   * @returns {Promise<*>}
   */
  async currencyPay (params) {
    if (!params.sessionKey) throw new Error('接口 currencyPay 的参数 sessionKey 必填')
    if (!params.openid) throw new Error('参数 openid 必填')
    if (!params.userIp) throw new Error('参数 userIp 必填')
    if (!params.amount) throw new Error('参数 amount 必填')
    if (!params.orderId) throw new Error('参数 outTradeNo 必填')
    if (!params.deviceType) throw new Error('参数 deviceType 必填')
    return await this._request('/xpay/currency_pay', params, 'POST')
  }

  /**
   * 代币支付退款(currency_pay接口的逆操作)
   * @param {String} params.openid 用户的openid
   * @param {String} params.userIp 用户ip
   * @param {Number} params.amount 退款金额
   * @param {String} params.outTradeNo 订单号pay_order_id
   * @param {String} params.outRefundNo 本次退款单的单号 order_id
   * @param {Number} params.deviceType 平台类型1-安卓 2-苹果
   * @returns {Promise<*>}
   */
  async cancelCurrencyPay (params) {
    if (!params.openid) throw new Error('参数 openid 必填')
    if (!params.userIp) throw new Error('参数 userIp 必填')
    if (!params.amount) throw new Error('参数 amount 必填')
    if (!params.payOrderId) throw new Error('参数 outTradeNo 必填')
    if (!params.orderId) throw new Error('参数 outRefundNo 必填')
    if (!params.deviceType) throw new Error('参数 deviceType 必填')
    return await this._request('/xpay/cancel_currency_pay', params, 'POST')
  }

  /**
   * 代币赠送接口，由于目前不支付按单号查赠送单的功能，所以当需要赠送的时候可以一直重试到返回0或者返回268490004（重复操作）为止
   * @param {String} params.openid 用户的openid
   * @param {String} params.userIp 用户ip
   * @param {String} params.outTradeNo 赠送单号order_id
   * @param {Number} params.amount 赠送金额
   * @param {Number} params.deviceType 平台类型1-安卓 2-苹果
   * @returns {Promise<*>}
   */
  async presentCurrency (params) {
    if (!params.openid) throw new Error('参数 openid 必填')
    if (!params.userIp) throw new Error('参数 userIp 必填')
    if (!params.orderId) throw new Error('参数 outTradeNo 必填')
    if (!params.amount) throw new Error('参数 amount 必填')
    if (!params.deviceType) throw new Error('参数 deviceType 必填')
    return await this._request('/xpay/present_currency', params, 'POST')
  }
}

export default Payment
